﻿using System;
using System.Collections;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Collections.Generic;

namespace SimpleSocket
{
    /// <summary>
    /// Use this class to start listening on a port, it will create instance of your Client class for each new connection.
    /// </summary>
    /// <example> This sample shows how to start listening on a port.
    /// <code><![CDATA[
    /// Server<myClient> myServer = new Server<myClient>();
    /// myServer.Start(101);
    /// ]]></code>
    /// </example>
    /// <typeparam name="T">Your client class that is derived from SimpleSocket.Client</typeparam>
    public class Server<T> where T : Client, new()
    {

        private Socket mainSocket;
        private List<Client> workerSocket = new List<Client>();

        internal AsyncCallback pfnWorkerCallBack;

        /// <summary>
        /// Starts the server listening on the selected port.
        /// </summary>
        /// <param name="port">Port number to bind</param>
        public void Start(int port)
        {
            Start(port, IPAddress.Any);
        }
        /// <summary>
        /// Starts the server listening on the selected port and IP address.
        /// </summary>
        /// <param name="port">Port number to bind</param>
        /// <param name="ip">IP address to bind</param>
        public void Start(int port, IPAddress ip)
        {
            mainSocket = new Socket(AddressFamily.InterNetwork,
                                          SocketType.Stream,
                                          ProtocolType.Tcp);
            IPEndPoint ipLocal = new IPEndPoint(ip, port);

            mainSocket.Bind(ipLocal);
            //  Start
            mainSocket.Listen(4);
            // Create call back for client connections...
            mainSocket.BeginAccept(new AsyncCallback(OnClientConnect), null);
        }

        internal void OnClientConnect(IAsyncResult asyn)
        {
            try
            {
                // Here we complete/end the BeginAccept() asynchronous call
                // by calling EndAccept() - which returns the reference to
                // a new Socket object
                Client newClient = (Client)new T();
                newClient.Socket = mainSocket.EndAccept(asyn);

                workerSocket.Add(newClient);

                // Let the worker Socket do the further processing for the 
                // just connected client
                WaitForData(newClient);

                // Since the main Socket is now free, it can go back and wait for
                // other clients who are attempting to connect
                mainSocket.BeginAccept(new AsyncCallback(OnClientConnect), null);

            }
            catch (ObjectDisposedException)
            {
                System.Diagnostics.Debugger.Log(0, "1", "\n OnClientConnection: Socket has been closed\n");
            }
            catch (SocketException se)
            {
                System.Diagnostics.Debugger.Log(0,"1",se.Message);
            }

        }

        internal void WaitForData(Client client)
        {
            try
            {
                if (pfnWorkerCallBack == null)
                {
                    // Specify the call back function which is to be 
                    // invoked when there is any write activity by the 
                    // connected client
                    pfnWorkerCallBack = new AsyncCallback(OnDataReceived);
                }
                SocketPacket theSocPkt = new SocketPacket();
                theSocPkt.currentClient = client;
                // Start receiving any data written by the connected client
                // asynchronously

                client.Socket.BeginReceive(theSocPkt.dataBuffer, 0,
                                   theSocPkt.dataBuffer.Length,
                                   SocketFlags.None,
                                   pfnWorkerCallBack,
                                   theSocPkt);
            }
            catch (SocketException se)
            {
                System.Diagnostics.Debugger.Log(0, "1", se.Message);
            }

        }

        // This is the call back function which will be invoked when the socket
        // detects any client writing of data on the stream
        internal void OnDataReceived(IAsyncResult asyn)
        {
            try
            {
                SocketPacket socketData = (SocketPacket)asyn.AsyncState;

                int iRx = 0;
                // Complete the BeginReceive() asynchronous call by EndReceive() method
                // which will return the number of characters written to the stream 
                // by the client
                iRx = socketData.currentClient.Socket.EndReceive(asyn);
                char[] chars = new char[iRx + 1];
                System.Text.Decoder d = System.Text.Encoding.UTF8.GetDecoder();
                int charLen = d.GetChars(socketData.dataBuffer,
                                         0, iRx, chars, 0);
                System.String szData = new System.String(chars);
                if (string.Equals(szData, "\0"))
                {
                    workerSocket.Remove(socketData.currentClient);
                    socketData.currentClient.Socket.Close();
                    socketData.currentClient.Disconnected();
                }
                else
                {
                    socketData.currentClient.ReceiveData(szData);

                    // Continue the waiting for data on the Socket
                    WaitForData(socketData.currentClient);
                }
            
            }
            catch (ObjectDisposedException)
            {
                System.Diagnostics.Debugger.Log(0, "1", "\nOnDataReceived: Socket has been closed\n");
            }
            catch (SocketException se)
            {
                System.Diagnostics.Debugger.Log(0, "1", se.Message);
            }
        }

        internal class SocketPacket
        {
            public Client currentClient;
            public byte[] dataBuffer = new byte[1];
        }
    }

}
